package com.qsw.fileoperatelib.iedata;

import android.util.Log;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.List;
import java.util.function.Predicate;

/**
 * 基础数据注入与提取工具
 */
public abstract class BIEDataUtil {
    protected static final Charset DEFAULT_STRING_CHARSET = StandardCharsets.UTF_8;// 默认String编码格式

    protected static final byte MD5_BYTES_LENGTH = 16;// MD5原始字节数组长度
    protected static final byte ORIGINAL_FILE_LENGTH_NUMBER_LENGTH = Long.BYTES;// 记录原始文件大小的数字长度

    protected static final byte MAX_NUMBER_MESSAGE = Byte.MAX_VALUE;// 注入的最大信息条数
    protected static final byte MAX_LENGTH_ONE_MESSAGE = Byte.MAX_VALUE;// 每条信息的最大长度

    /**
     * 操作类型
     */
    public enum IEType {
        DATA_READ,// 注入数据
        DATA_WRITE,// 读取数据
        IEFILE_CREATE,// 新文件生成
        IEFILE_RECOVER// 原始文件恢复
    }

    /**
     * 向文件中注入信息
     *
     * @param file                     待注入信息的文件
     * @param dataList                 待注入的信息
     * @param onProgressChangeListener 进度回调
     * @return 注入信息后的文件，若为null则操作失败
     * @throws IOException              异常
     * @throws NoSuchAlgorithmException 异常
     */
    public File inject(File file, List<String> dataList, OnProgressChangeListener onProgressChangeListener) throws IOException, NoSuchAlgorithmException {
        return inject(file, dataList, null, onProgressChangeListener);
    }

    /**
     * 向文件中注入信息
     *
     * @param file                     待注入信息的文件
     * @param dataList                 待注入的信息
     * @param newSuffix                注入后的文件后缀，若为null，则不重命名
     * @param onProgressChangeListener 进度回调
     * @return 注入信息后的文件，若为null则操作失败
     * @throws IOException              异常
     * @throws NoSuchAlgorithmException 异常
     */
    public File inject(File file, List<String> dataList, String newSuffix, OnProgressChangeListener onProgressChangeListener) throws IOException, NoSuchAlgorithmException {
        return inject(file, dataList, null, newSuffix, false, onProgressChangeListener);
    }

    /**
     * 向文件中注入信息
     *
     * @param file                     待注入信息的文件
     * @param dataList                 待注入的信息
     * @param copyToFolder             不修改原文件，复制原文件并注入后保存在该目录下，若为null，则不复制直接修改原文件
     * @param newSuffix                注入后的文件后缀，若为null，则不重命名
     * @param deleteIfExist            true：若新文件存在则删除；false：若新文件存在则返回失败
     * @param onProgressChangeListener 进度回调
     * @return 注入信息后的文件，若为null则操作失败
     * @throws IOException              异常
     * @throws NoSuchAlgorithmException 异常
     */
    public File inject(File file, List<String> dataList, String copyToFolder, String newSuffix, boolean deleteIfExist,
                       OnProgressChangeListener onProgressChangeListener) throws IOException, NoSuchAlgorithmException {
        if (file == null || !file.exists() || !file.isFile()) {
            return null;
        }

        // 数据预处理，筛除空数据
        dataList.removeIf(new Predicate<String>() {
            @Override
            public boolean test(String str) {
                return str == null || str.length() == 0;
            }
        });

        RandomAccessFile randomAccessFile = null;
        boolean writeDataResult = false;
        File targetFile = file;
        try {
            randomAccessFile = new RandomAccessFile(targetFile, "rw");

            // 检查是否符合注入条件
            if (!checkInjectState(randomAccessFile, dataList)) {
                return null;
            }

            // 获取原始文件的MD5
            byte[] md5Bytes = getMD5Bytes(targetFile);
            if (md5Bytes == null || md5Bytes.length != MD5_BYTES_LENGTH) {
                return null;
            }

            if (copyToFolder != null) {
                targetFile = copyToFolder(randomAccessFile, copyToFolder, getFileNameOfNewSuffix(file.getName(), newSuffix), deleteIfExist,
                        onProgressChangeListener != null ? new OnSingleTypeProgressListener() {
                            @Override
                            public void onSingleTypeProgress(long progress, long maxProgress) {
                                onProgressChangeListener.onProgressChange(IEType.IEFILE_CREATE, progress, maxProgress, progress, maxProgress);
                            }
                        } : null);
                if (targetFile == null) {
                    return null;
                }
                randomAccessFile.close();
                randomAccessFile = new RandomAccessFile(targetFile, "rw");
            }

            // 注入数据
            writeDataResult = writeData(randomAccessFile, md5Bytes, dataList,
                    onProgressChangeListener != null ? new OnSingleTypeProgressListener() {
                        @Override
                        public void onSingleTypeProgress(long progress, long maxProgress) {
                            onProgressChangeListener.onProgressChange(IEType.DATA_WRITE, progress, maxProgress, progress, maxProgress);
                        }
                    } : null);
        } finally {
            if (randomAccessFile != null) {
                randomAccessFile.close();
            }
        }

        if (writeDataResult && copyToFolder == null && newSuffix != null) {
            targetFile = changeFileSuffix(targetFile, newSuffix);
        }

        return writeDataResult ? targetFile : null;
    }

    /**
     * 检查是否符合注入条件
     *
     * @param randomAccessFile 待检查的文件
     * @param dataList         待注入的数据源
     * @return true：可注入；false：不可注入
     * @throws IOException 异常
     */
    protected boolean checkInjectState(RandomAccessFile randomAccessFile, List<String> dataList) throws IOException {
        if (isInjectFile(randomAccessFile)) {
            return false;
        }

        if (dataList != null && dataList.size() > MAX_NUMBER_MESSAGE) {
            return false;
        }

        if (dataList != null && dataList.size() > 0) {
            for (String str : dataList) {
                if (str == null || str.getBytes(DEFAULT_STRING_CHARSET).length > MAX_LENGTH_ONE_MESSAGE) {
                    return false;
                }
            }
        }

        return true;
    }

    /**
     * 检查是否为已注入文件
     *
     * @param file 待检查的文件
     * @return true：已注入；false：未注入
     * @throws IOException 异常
     */
    public boolean isInjectFile(File file) throws IOException {
        RandomAccessFile randomAccessFile = null;
        boolean isInjectFile = false;
        try {
            randomAccessFile = new RandomAccessFile(file, "rw");
            isInjectFile = isInjectFile(randomAccessFile);
        } finally {
            if (randomAccessFile != null) {
                randomAccessFile.close();
            }
        }
        return isInjectFile;
    }

    /**
     * 检查是否为已注入文件
     *
     * @param randomAccessFile 待检查的文件
     * @return true：已注入；false：未注入
     * @throws IOException 异常
     */
    protected abstract boolean isInjectFile(RandomAccessFile randomAccessFile) throws IOException;

    /**
     * 向文件中注入信息
     *
     * @param randomAccessFile             待注入信息的文件
     * @param fileMD5Bytes                 待注入文件的原始MD5
     * @param dataList                     待注入的数据
     * @param onSingleTypeProgressListener 进度回调
     * @return 是否成功
     * @throws IOException 异常
     */
    protected abstract boolean writeData(RandomAccessFile randomAccessFile, byte[] fileMD5Bytes,
                                         List<String> dataList, OnSingleTypeProgressListener onSingleTypeProgressListener) throws IOException;

    /**
     * 读取注入的数据并恢复注入文件为原始文件
     *
     * @param file                     待处理文件
     * @param checkMD5                 true：自动对恢复后的文件做MD5校验
     * @param onProgressChangeListener 进度回调
     * @return 读取到的数据，如果出错则返回null
     * @throws IOException 异常
     */
    public ReadDataAndRecoverResult readDataAndRecover(File file, boolean checkMD5,
                                                       OnProgressChangeListener onProgressChangeListener) throws IOException, NoSuchAlgorithmException {
        return readDataAndRecover(file, null, checkMD5, onProgressChangeListener);
    }

    /**
     * 读取注入的数据并恢复注入文件为原始文件
     *
     * @param file                     待处理文件
     * @param recoverSuffix            恢复后的文件后缀
     * @param checkMD5                 true：自动对恢复后的文件做MD5校验
     * @param onProgressChangeListener 进度回调
     * @return 读取到的数据，如果出错则返回null
     * @throws IOException 异常
     */
    public ReadDataAndRecoverResult readDataAndRecover(File file, String recoverSuffix,
                                                       boolean checkMD5, OnProgressChangeListener onProgressChangeListener) throws IOException, NoSuchAlgorithmException {
        return readDataAndRecover(file, null, recoverSuffix, false, checkMD5, onProgressChangeListener);
    }

    /**
     * 读取注入的数据并恢复注入文件为原始文件
     *
     * @param file                     待处理文件
     * @param copyToFolder             不修改原文件，复制原文件并注入后保存在该目录下，若为null，则不复制直接修改原文件
     * @param recoverSuffix            恢复后的文件后缀
     * @param deleteIfExist            true：若新文件存在则删除；false：若新文件存在则返回失败
     * @param checkMD5                 true：自动对恢复后的文件做MD5校验
     * @param onProgressChangeListener 进度回调
     * @return 读取到的数据，如果出错则返回null
     * @throws IOException 异常
     */
    public ReadDataAndRecoverResult readDataAndRecover(File file, String copyToFolder, String recoverSuffix, boolean deleteIfExist,
                                                       boolean checkMD5, OnProgressChangeListener onProgressChangeListener) throws IOException, NoSuchAlgorithmException {
        if (file == null || !file.exists() || !file.isFile()) {
            return null;
        }

        RandomAccessFile randomAccessFile = null;
        ReadDataResult readDataResult;
        File targetFile = file;
        try {
            randomAccessFile = new RandomAccessFile(file, "rw");

            readDataResult = readData(randomAccessFile,
                    onProgressChangeListener != null ? new OnSingleTypeProgressListener() {
                        @Override
                        public void onSingleTypeProgress(long progress, long maxProgress) {
                            onProgressChangeListener.onProgressChange(IEType.DATA_READ, progress, maxProgress, progress, maxProgress);
                        }
                    } : null);
            if (readDataResult == null) {
                return null;
            }
            targetFile = recoverFile(file, copyToFolder, recoverSuffix, deleteIfExist,
                    checkMD5 ? readDataResult.getRecoverFilePathMD5() : null, onProgressChangeListener);
        } finally {
            if (randomAccessFile != null) {
                randomAccessFile.close();
            }
        }
        return (readDataResult != null && targetFile != null)
                ? new ReadDataAndRecoverResult(readDataResult, targetFile.getAbsolutePath())
                : null;
    }

    /**
     * 读取数据
     *
     * @param file                         文件
     * @param onSingleTypeProgressListener 进度
     * @return 读取到的数据，如果出错则返回null
     * @throws IOException 异常
     */
    public ReadDataResult readData(File file, OnSingleTypeProgressListener onSingleTypeProgressListener) throws IOException {
        if (file == null || !file.exists() || !file.isFile()) {
            return null;
        }

        RandomAccessFile randomAccessFile = null;
        ReadDataResult readDataResult;
        try {
            randomAccessFile = new RandomAccessFile(file, "r");

            readDataResult = readData(randomAccessFile, onSingleTypeProgressListener);
        } finally {
            if (randomAccessFile != null) {
                randomAccessFile.close();
            }
        }

        return readDataResult;
    }

    /**
     * 读取数据
     *
     * @param randomAccessFile             文件
     * @param onSingleTypeProgressListener 进度回调
     * @return 读取到的数据，如果出错则返回null
     * @throws IOException 异常
     */
    protected abstract ReadDataResult readData(RandomAccessFile randomAccessFile, OnSingleTypeProgressListener onSingleTypeProgressListener) throws IOException;

    /**
     * 恢复注入文件为原始文件
     *
     * @param file                     待恢复文件
     * @param checkMD5Bytes            若为null则不做校验，否则将对恢复后的文件做MD5校验，若校验失败则认为操作失败
     * @param onProgressChangeListener 进度
     * @return 恢复后的文件，若为null则操作失败
     * @throws IOException 异常
     */
    public File recoverFile(File file, byte[] checkMD5Bytes, OnProgressChangeListener onProgressChangeListener) throws IOException, NoSuchAlgorithmException {
        return recoverFile(file, null, checkMD5Bytes, onProgressChangeListener);
    }

    /**
     * 恢复注入文件为原始文件
     *
     * @param file                     待恢复文件
     * @param recoverSuffix            恢复后的文件后缀
     * @param checkMD5Bytes            若为null则不做校验，否则将对恢复后的文件做MD5校验，若校验失败则认为操作失败
     * @param onProgressChangeListener 进度
     * @return 恢复后的文件，若为null则操作失败
     * @throws IOException 异常
     */
    public File recoverFile(File file, String recoverSuffix, byte[] checkMD5Bytes, OnProgressChangeListener onProgressChangeListener) throws IOException, NoSuchAlgorithmException {
        return recoverFile(file, null, recoverSuffix, false, checkMD5Bytes, onProgressChangeListener);
    }

    /**
     * 恢复注入文件为原始文件
     *
     * @param file                     待恢复文件
     * @param copyToFolder             不修改原文件，复制原文件并注入后保存在该目录下，若为null，则不复制直接修改原文件
     * @param recoverSuffix            恢复后的文件后缀
     * @param deleteIfExist            true：若新文件存在则删除；false：若新文件存在则返回失败
     * @param checkMD5Bytes            若为null则不做校验，否则将对恢复后的文件做MD5校验，若校验失败则认为操作失败
     * @param onProgressChangeListener 进度
     * @return 恢复后的文件，若为null则操作失败
     * @throws IOException 异常
     */
    public File recoverFile(File file, String copyToFolder, String recoverSuffix, boolean deleteIfExist,
                            byte[] checkMD5Bytes, OnProgressChangeListener onProgressChangeListener) throws IOException, NoSuchAlgorithmException {
        if (file == null || !file.exists() || !file.isFile()) {
            return null;
        }

        RandomAccessFile randomAccessFile = null;
        boolean result = false;
        File targetFile = file;
        try {
            randomAccessFile = new RandomAccessFile(file, "rw");

            if (copyToFolder != null) {
                targetFile = copyToFolder(randomAccessFile, copyToFolder, getFileNameOfNewSuffix(file.getName(), recoverSuffix), deleteIfExist,
                        onProgressChangeListener != null ? new OnSingleTypeProgressListener() {
                            @Override
                            public void onSingleTypeProgress(long progress, long maxProgress) {
                                onProgressChangeListener.onProgressChange(IEType.IEFILE_CREATE, progress, maxProgress, progress, maxProgress);
                            }
                        } : null);
                if (targetFile == null) {
                    return null;
                }
                randomAccessFile.close();
                randomAccessFile = new RandomAccessFile(targetFile, "rw");
            }

            result = recoverFile(randomAccessFile, onProgressChangeListener != null ? new OnSingleTypeProgressListener() {
                @Override
                public void onSingleTypeProgress(long progress, long maxProgress) {
                    onProgressChangeListener.onProgressChange(IEType.IEFILE_RECOVER, progress, maxProgress, progress, maxProgress);
                }
            } : null);

            if (checkMD5Bytes != null) {
                result = Arrays.equals(checkMD5Bytes, getMD5Bytes(targetFile));
            }
        } finally {
            if (randomAccessFile != null) {
                randomAccessFile.close();
            }
        }
        if (result && copyToFolder == null && recoverSuffix != null) {
            targetFile = changeFileSuffix(targetFile, recoverSuffix);
        }
        return result ? targetFile : null;
    }

    /**
     * 恢复注入文件为原始文件
     *
     * @param randomAccessFile             待恢复文件
     * @param onSingleTypeProgressListener 进度回调
     * @return true：成功；false：失败
     * @throws IOException 异常
     */
    protected abstract boolean recoverFile(RandomAccessFile randomAccessFile, OnSingleTypeProgressListener onSingleTypeProgressListener) throws IOException;

    /**
     * 获取文件的MD5
     *
     * @param file 文件
     * @return 文件的MD5
     * @throws NoSuchAlgorithmException 异常
     * @throws IOException              异常
     */
    private static byte[] getMD5Bytes(File file) throws NoSuchAlgorithmException, IOException {
        if (file == null || !file.exists()) {
            return null;
        }
        MessageDigest md5 = MessageDigest.getInstance("MD5");
        FileInputStream fis = new FileInputStream(file);

        byte[] dataBytes = new byte[1024];
        int readLength;
        while ((readLength = fis.read(dataBytes)) != -1) {
            md5.update(dataBytes, 0, readLength);
        }
        byte[] md5Array = md5.digest();

        fis.close();

        return md5Array;
    }

    /**
     * 获取文件的MD5
     *
     * @param file 文件
     * @return 文件的MD5
     * @throws NoSuchAlgorithmException 异常
     * @throws IOException              异常
     */
    private static String getMD5(File file) throws NoSuchAlgorithmException, IOException {
        if (file == null || !file.exists()) {
            return null;
        }
        MessageDigest md5 = MessageDigest.getInstance("MD5");
        FileInputStream fis = new FileInputStream(file);

        byte[] dataBytes = new byte[1024];
        int readLength;
        while ((readLength = fis.read(dataBytes)) != -1) {
            md5.update(dataBytes, 0, readLength);
        }
        byte[] md5Array = md5.digest();

        fis.close();

        return bytesToHex(md5Array);
    }

    /**
     * 将字节数组转换为十六进制字符串。
     * 每一个byte转换为两个十六进制数，比如一个byte将被转换为AB或ab
     *
     * @param byteArray 字节数组
     * @return 转换后的十六进制字符串
     */
    private static String bytesToHex(byte[] byteArray) {
        return bytesToHex(byteArray, byteArray.length, false, false, true);
    }

    /**
     * 将字节数组转换为十六进制字符串。
     * 每一个byte转换为两个十六进制数，比如一个byte将被转换为AB或ab
     *
     * @param byteArray  字节数组
     * @param byteLength 需要转换的数组长度
     * @param prefix     true：添加0x前缀；false：不添加前缀
     * @param interval   true：天极爱空格间隔；false：不添加间隔
     * @param upperCase  true：大写；false：跟随源数据
     * @return 转换后的十六进制字符串
     */
    private static String bytesToHex(byte[] byteArray, int byteLength, boolean prefix, boolean interval, boolean upperCase) {
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < byteArray.length && i < byteLength; i++) {
            String s = Integer.toHexString(byteArray[i] & 0xFF);
            if (s.length() < 2) {
                s = "0" + s;
            }
            if (upperCase) {
                s = s.toUpperCase();
            }
            if (prefix) {
                s = "0x" + s;
            }
            if (interval) {
                s += " ";
            }
            sb.append(s);
        }
        return interval ? sb.substring(0, sb.length() - 1) : sb.toString();
    }

    /**
     * 修改文件后缀
     *
     * @param file      待修改文件
     * @param newSuffix 新的后缀
     * @return true：成功；false：失败
     */
    private static File changeFileSuffix(File file, String newSuffix) {
        if (file == null || !file.exists()) {
            return null;
        }

        // 获取文件原来的目录
        String directoryPath = file.getParent();
        // 新的文件名
        String newFileName = getFileNameOfNewSuffix(file.getName(), newSuffix);

        // 创建新的文件对象
        File newFile = new File(directoryPath, newFileName);
        // 重命名文件
        return file.renameTo(newFile) ? newFile : null;
    }

    /**
     * 获取修改后缀后的文件名
     *
     * @param fileName  待修改文件名
     * @param newSuffix 新的后缀
     * @return 修改后的文件名
     */
    private static String getFileNameOfNewSuffix(String fileName, String newSuffix) {
        if (fileName == null) {
            return newSuffix;
        }

        // 获取文件名字不包含后缀的部分
        int lastIndexOfPoint = fileName.lastIndexOf(".");
        String fileNameWithoutSuffix = lastIndexOfPoint < 0 ? fileName : fileName.substring(0, lastIndexOfPoint);
        // 新的文件名
        return fileNameWithoutSuffix + ((newSuffix == null) ? "" : ("." + newSuffix));
    }

    /**
     * 复制文件，若目标文件不存在则创建
     *
     * @param randomAccessFile             原文件
     * @param targetFolder                 复制后文件目录
     * @param targetFileName               复制后文件名
     * @param deleteIfExist                true：若新文件存在则删除；false：若新文件存在则返回失败
     * @param onSingleTypeProgressListener 当文件保存进度改变时的回调
     * @return 复制后的文件，若为null则失败
     * @throws IOException 异常
     */
    private static File copyToFolder(RandomAccessFile randomAccessFile, String targetFolder, String targetFileName,
                                     boolean deleteIfExist, OnSingleTypeProgressListener onSingleTypeProgressListener) throws IOException {
        File file = new File(targetFolder);
        if (!file.exists()) {
            if (!file.mkdirs()) {
                return null;
            }
        }
        file = new File(targetFolder, targetFileName);
        if (file.exists()) {
            if (!deleteIfExist || !file.delete()) {
                return null;
            }
        }
        if (!file.createNewFile()) {
            return null;
        }

        randomAccessFile.seek(0);
        long maxLength = randomAccessFile.length();
        FileOutputStream fos = new FileOutputStream(file);
        byte[] buffer = new byte[1024];
        int savedSize = 0;
        int readSize;
        while ((readSize = randomAccessFile.read(buffer)) > 0) {
            fos.write(buffer, 0, readSize);
            fos.flush();
            if (onSingleTypeProgressListener != null) {
                savedSize += readSize;
                onSingleTypeProgressListener.onSingleTypeProgress(savedSize, maxLength);
            }
        }

        fos.close();

        return file;
    }

    public static class ReadDataAndRecoverResult {
        private ReadDataResult readDataResult;
        private String recoverFilePath;

        public ReadDataAndRecoverResult() {
        }

        public ReadDataAndRecoverResult(ReadDataResult readDataResult, String recoverFilePath) {
            this.readDataResult = readDataResult;
            this.recoverFilePath = recoverFilePath;
        }

        public ReadDataResult getReadDataResult() {
            return readDataResult;
        }

        public void setReadDataResult(ReadDataResult readDataResult) {
            this.readDataResult = readDataResult;
        }

        public String getRecoverFilePath() {
            return recoverFilePath;
        }

        public void setRecoverFilePath(String recoverFilePath) {
            this.recoverFilePath = recoverFilePath;
        }
    }

    public static class ReadDataResult {
        private List<String> readData;
        private byte[] recoverFilePathMD5;

        public ReadDataResult() {
        }

        public ReadDataResult(List<String> readData, byte[] recoverFilePathMD5) {
            this.readData = readData;
            this.recoverFilePathMD5 = recoverFilePathMD5;
        }

        public List<String> getReadData() {
            return readData;
        }

        public void setReadData(List<String> readData) {
            this.readData = readData;
        }

        public byte[] getRecoverFilePathMD5() {
            return recoverFilePathMD5;
        }

        public void setRecoverFilePathMD5(byte[] recoverFilePathMD5) {
            this.recoverFilePathMD5 = recoverFilePathMD5;
        }
    }

    /**
     * 当进度改变时的回调
     */
    public interface OnSingleTypeProgressListener {
        /**
         * 当进度改变时的回调
         *
         * @param progress    进度
         * @param maxProgress 最大进度
         */
        void onSingleTypeProgress(long progress, long maxProgress);
    }

    /**
     * 操作进度回调
     */
    public interface OnProgressChangeListener {
        /**
         * 操作进度回调
         *
         * @param ieType             当前操作类型
         * @param ieTypeCurrProgress 当前操作步骤进度
         * @param ieTypeMaxProgress  当前操作步骤最大进度
         * @param totalCurrProgress  总流程进度
         * @param totalMaxProgress   总流程最大进度
         */
        void onProgressChange(IEType ieType, long ieTypeCurrProgress, long ieTypeMaxProgress, long totalCurrProgress, long totalMaxProgress);
    }
}
